package com.gwtent.serialization.client.json;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;

import org.phpgwt.annotations.DateFormat;
import org.phpgwt.phpgenerator.PHPGenerator;

import com.google.gwt.i18n.client.DateTimeFormat;
import com.google.gwt.json.client.JSONArray;
import com.google.gwt.json.client.JSONBoolean;
import com.google.gwt.json.client.JSONException;
import com.google.gwt.json.client.JSONNull;
import com.google.gwt.json.client.JSONNumber;
import com.google.gwt.json.client.JSONObject;
import com.google.gwt.json.client.JSONParser;
import com.google.gwt.json.client.JSONString;
import com.google.gwt.json.client.JSONValue;
import com.gwtent.common.client.ObjectFactory;
import com.gwtent.reflection.client.ClassType;
import com.gwtent.reflection.client.Constructor;
import com.gwtent.reflection.client.Field;
import com.gwtent.reflection.client.Method;
import com.gwtent.reflection.client.ReflectionUtils;
import com.gwtent.reflection.client.TypeOracle;
import com.gwtent.serialization.client.DataContract;
import com.gwtent.serialization.client.DoubleConvert;

public class JsonSerializer {

	public static JSONValue serialize(Object obj) {
		if (obj != null) {
			if (obj instanceof Iterable) {
				return serializeIterable((Iterable) obj);
			} else if (isSingle(obj.getClass().getName())) {
				return serializeSingle(obj);
			} else if (hasReflection(obj.getClass())) {
				return serializeObject(obj);
			}
		}

		return JSONNull.getInstance();
	}

	public static JSONValue serializeObject(Object object) {
		if (object != null && hasReflection(object.getClass())) {
			ClassType type = TypeOracle.Instance.getClassType(object.getClass().getName());

			JSONObject result = new JSONObject();

			for (Field field : type.getFields()) {
				Method getter = findGetter(field, type);

				if (getter != null) {
					Object value = getter.invoke(object);

					if (value != null && ("java.util.Date".equals(field.getType().getSimpleSourceName()) || "java.sql.Date".equals(field.getType()
							.getSimpleSourceName())) && field.isAnnotationPresent(DateFormat.class)) {
						DateFormat dateFormat = field.getAnnotation(DateFormat.class);
						result.put(field.getName(), new JSONString(DateTimeFormat.getFormat(dateFormat.value()).format((Date) value)));
					} else {
						result.put(field.getName(), serialize(value));
					}
				}
			}

			return result;
		} else {
			return JSONNull.getInstance();
		}
	}

	public static JSONValue serializeIterable(Iterable objects) {
		JSONArray result = new JSONArray();

		int index = 0;
		for (Object obj : objects) {
			result.set(index, serialize(obj));
			index++;
		}
		return result;
	}

	public static JSONValue serializeSingle(Object object) {
		if (Character.class.getName().equals(object.getClass().getName())) {
			return new JSONString(((Character) object).toString());
		} else if (Date.class.getName().equals(object.getClass().getName())) {
			return new JSONString(((Date) object).toString());
		} else if (String.class.getName().equals(object.getClass().getName())) {
			return new JSONString((String) object);
		} else if (Boolean.class.getName().equals(object.getClass().getName())) {
			return JSONBoolean.getInstance((Boolean) object);
		} else if (isSingle(object.getClass().getName())) {
			return new JSONNumber(Double.parseDouble("" + object));
		}

		return null;
	}

	public static Object deserializeSingle(JSONValue value, Class clazz) throws JSONException {
		// primitive (wrapper)

		if (Integer.class.equals(clazz)) {
			final JSONNumber jsonNumber = value.isNumber();

			if (jsonNumber != null) {
				return (int) Math.floor(jsonNumber.doubleValue());
			}
		} else if (Boolean.class.equals(clazz)) {
			final JSONBoolean jsonBoolean = value.isBoolean();

			if (jsonBoolean != null) {
				return jsonBoolean.booleanValue();
			} else {
				JSONNumber jsonNumber = value.isNumber();

				if (jsonNumber != null) {
					if (0 == jsonNumber.doubleValue()) {
						return false;
					} else {
						return true;
					}
				}
			}
		} else if (Byte.class.equals(clazz)) {
			final JSONNumber jsonNumber = value.isNumber();

			if (jsonNumber != null) {
				return (byte) Math.floor(jsonNumber.doubleValue());
			}
		} else if (Character.class.equals(clazz)) {
			final JSONNumber jsonNumber = value.isNumber();

			if (jsonNumber != null) {
				return (char) Math.floor(jsonNumber.doubleValue());
			} else {
				final JSONString jsonString = value.isString();

				if (jsonString != null) {
					final String stringValue = jsonString.stringValue();

					if (stringValue.length() == 1) {
						return stringValue.charAt(0);
					} else {
						throw new JSONException("Invalid JSON value for character data:" + stringValue);
					}
				}
			}
		} else if (Double.class.equals(clazz)) {
			final JSONNumber jsonNumber = value.isNumber();

			if (jsonNumber != null) {
				return jsonNumber.doubleValue();
			}
		} else if (Float.class.equals(clazz)) {
			final JSONNumber jsonNumber = value.isNumber();

			if (jsonNumber != null) {
				return (float) jsonNumber.doubleValue();
			}
		} else if (Long.class.equals(clazz)) {
			final JSONNumber jsonNumber = value.isNumber();

			if (jsonNumber != null) {
				return (long) Math.floor(jsonNumber.doubleValue());
			}
		} else if (Short.class.equals(clazz)) {
			final JSONNumber jsonNumber = value.isNumber();

			if (jsonNumber != null) {
				return (short) Math.floor(jsonNumber.doubleValue());
			}
		}

		// object

		else if (String.class.equals(clazz)) {
			final JSONString jsonString = value.isString();

			if (jsonString != null) {
				return jsonString.stringValue();
			}
		} else if (java.util.Date.class.equals(clazz) || java.sql.Date.class.equals(clazz)) {
			final JSONString jsonString = value.isString();

			if (jsonString != null) {
				return DateParser.parse(jsonString.stringValue());
			}
		}

		return null;
	}

	public static Object deserializeSingle(String json, Class clazz) throws JSONException {
		JSONValue value = JSONParser.parse(json);

		if (value != null) {
			return deserializeSingle(value, clazz);
		} else {
			return null;
		}
	}

	public static void deserializeList(List list, String json, Class clazz) throws JSONException {
		JSONValue value = JSONParser.parse(json);
		deserializeList(list, value, clazz);
	}

	public static void deserializeList(List list, JSONValue value, Class clazz) throws JSONException {
		if (value != null) {
			final JSONArray jsonArray = value.isArray();

			if (jsonArray != null) {

				if (isSingle(clazz.getName())) {
					for (int i = 0; i < jsonArray.size(); i++) {
						list.add(deserializeSingle(jsonArray.get(i), clazz));
					}
				} else if (hasReflection(clazz)) {
					ClassType type = TypeOracle.Instance.getClassType(clazz);
					for (int i = 0; i < jsonArray.size(); i++) {
						list.add(deserializeObject(jsonArray.get(i), type));
					}
				}
			}
		}
	}

	public static <T extends Object> T deserializeObject(String json, Class<T> clazz) {
		ReflectionUtils.checkReflection(clazz);
		ClassType type = TypeOracle.Instance.getClassType(clazz);
		return (T) deserializeObject(json, type);
	}

	public static Object deserializeObject(String json, ClassType type) throws JSONException {
		JSONValue value = JSONParser.parse(json);

		Constructor constructor = type.findConstructor(new String[0]);
		Object result = constructor.newInstance();

		deserialize(value, result, type);

		return result;
	}

	public static Object deserializeObject(JSONValue value, ClassType type) {
		Constructor constructor = type.findConstructor(new String[0]);
		Object result = constructor.newInstance();

		deserialize(value, result, type);

		return result;
	}

	private static void deserialize(JSONValue value, Object obj, ClassType type) {
		if (value instanceof JSONArray) {
			if (obj instanceof Collection) {
				// Expect @DataContract(clazz=XXXX.class), XXXX is the object class we gona create
				ObjectFactory<Object> objectFactory = new ObjectReflectionFactory(type.getAnnotation(DataContract.class));
				deserializeArray((JSONArray) value, (Collection) obj, objectFactory);
			} else {
				throw new RuntimeException("JSONArray request a Collection object to contain it.");
			}
		} else if (value instanceof JSONObject) {
			deserializePureObject((JSONObject) value, obj);
		}
	}

	private static void deserializeArray(JSONArray array, Collection object, ObjectFactory<Object> objectFactory) {
		for (int i = 0; i < array.size(); i++) {
			Object objectItem = objectFactory.getObject();
			deserializePureObject((JSONObject) (array.get(i)), objectItem);
			object.add(objectItem);
		}
	}

	private static void deserializePureObject(JSONObject value, Object obj) {
		ClassType type = TypeOracle.Instance.getClassType(obj.getClass());

		for (Field field : type.getFields()) {
			Method setter = findSetter(field, type);

			if (setter != null) {
				if (value.containsKey(field.getName())) {
					JSONValue fieldValue = value.get(field.getName());
					Object fieldObject = null;

					if (fieldValue instanceof JSONObject) {
						fieldObject = deserializeObject(fieldValue, TypeOracle.Instance.getClassType(field.getDeclaringClass()));
						setter.invoke(obj, fieldObject);
					} else {
						if (fieldValue instanceof JSONNull) {
							setter.invoke(obj, new Object[] { null });
							continue;
						} else if (fieldValue instanceof JSONBoolean) {
							fieldObject = ((JSONBoolean) fieldValue).booleanValue();
						} else if (fieldValue instanceof JSONNumber) {
							fieldObject = ((JSONNumber) fieldValue).doubleValue();
						} else if (fieldValue instanceof JSONString) {
							fieldObject = ((JSONString) fieldValue).stringValue();
						}

						if (fieldValue instanceof JSONString && isNumeric(field.getTypeName())) {
							fieldValue = new JSONNumber(Double.parseDouble(fieldValue.isString().stringValue()));
						} else if (fieldValue instanceof JSONString
								&& (Boolean.class.getName().equals(field.getTypeName()) || "boolean".equals(field.getTypeName()))) {
							if ("1".equals(fieldValue.isString().stringValue())
									|| "true".equalsIgnoreCase(fieldValue.isString().stringValue())) {
								fieldObject = true;
							} else {
								fieldObject = false;
							}
						}

						if (fieldValue instanceof JSONNumber) {
							handleDouble(obj, field, setter, fieldValue);
						} else if ("char".equals(field.getType().getSimpleSourceName())
								|| "java.lang.Character".equals(field.getType().getSimpleSourceName())) {
							setter.invoke(obj, ((String) fieldObject).charAt(0));
						} else if ("java.util.Date".equals(field.getType().getSimpleSourceName())
								|| "java.sql.Date".equals(field.getType().getSimpleSourceName())) {
							setter.invoke(obj, DateParser.parse((String) fieldObject));
						} else if (fieldValue instanceof JSONArray) {
							// PARA
							List list = new ArrayList();
//							ClassType typeParam = TypeOracle.Instance.getClassType(field.getTypeParameterClass());
							deserializeList(list, fieldValue, field.getTypeParameterClass());
							setter.invoke(obj, list);
						} else {
							setter.invoke(obj, fieldObject);
						}
					}
				}
			}
		}
	}

	private static void handleDouble(Object instance, Field field, Method setter, JSONValue jsonValue) {
		Double doubleValue = ((JSONNumber) jsonValue).doubleValue();
		if ((instance instanceof DoubleConvert))
			((DoubleConvert) instance).convertDouble(field.getName(), doubleValue);
		else {
			if (field.getTypeName().equals(java.lang.Integer.class.getName()) || field.getTypeName().equals("int")) {
				setter.invoke(instance, doubleValue.intValue());
			} else if (field.getTypeName().equals(java.lang.Float.class.getName()) || field.getTypeName().equals("float")) {
				setter.invoke(instance, doubleValue.floatValue());
			} else if (field.getTypeName().equals(java.lang.Character.class.getName()) || field.getTypeName().equals("char")) {
				setter.invoke(instance, (char) doubleValue.intValue());
			} else if (field.getTypeName().equals(java.lang.Byte.class.getName()) || field.getTypeName().equals("byte")) {
				setter.invoke(instance, doubleValue.byteValue());
			} else if (field.getTypeName().equals(java.lang.Short.class.getName()) || field.getTypeName().equals("short")) {
				setter.invoke(instance, doubleValue.shortValue());
			} else if (field.getTypeName().equals(java.lang.Long.class.getName()) || field.getTypeName().equals("long")) {
				setter.invoke(instance, doubleValue.longValue());
			} else if (field.getTypeName().equals(java.lang.Double.class.getName()) || field.getTypeName().equals("double")) {
				setter.invoke(instance, doubleValue);
			} else if (field.getTypeName().equals(java.util.Date.class.getName())
					|| field.getTypeName().equals(java.sql.Date.class.getName())) {
				setter.invoke(instance, new Date(doubleValue.longValue()));
			}
		}
	}

	public static boolean isNumeric(String clazz) {
		if (Integer.class.getName().equals(clazz) || "int".equals(clazz)) {
			return true;
		} else if (Byte.class.getName().equals(clazz) || "byte".equals(clazz)) {
			return true;
		} else if (Short.class.getName().equals(clazz) || "short".equals(clazz)) {
			return true;
		} else if (Long.class.getName().equals(clazz) || "long".equals(clazz)) {
			return true;
		} else if (Float.class.getName().equals(clazz) || "float".equals(clazz)) {
			return true;
		} else if (Double.class.getName().equals(clazz) || "double".equals(clazz)) {
			return true;
		} else {
			return false;
		}
	}

	public static boolean isSingle(String clazz) {
		if (isNumeric(clazz)) {
			return true;
		} else if (Boolean.class.getName().equals(clazz) || "boolean".equals(clazz)) {
			return true;
		} else if (Character.class.getName().equals(clazz) || "char".equals(clazz)) {
			return true;
		} else if (Date.class.getName().equals(clazz)) {
			return true;
		} else if (String.class.getName().equals(clazz)) {
			return true;
		}

		return false;
	}

	private static Method findGetter(Field field, ClassType type) {
		for (Method method : type.getMethods()) {
			if (("get" + field.getName()).toLowerCase().equals(method.getName().toLowerCase())
					|| ("is" + field.getName()).toLowerCase().equals(method.getName().toLowerCase())) {
				return method;
			}
		}

		return null;
	}

	private static Method findSetter(Field field, ClassType type) {
		for (Method method : type.getMethods()) {
			if (("set" + field.getName()).toLowerCase().equals(method.getName().toLowerCase())) {
				return method;
			}
		}

		return null;
	}

	private static boolean hasReflection(Class clazz) {
		return TypeOracle.Instance.getClassType(clazz) != null;
	}
}
